package com.spzx.order.service.impl;

import java.math.BigDecimal;
import java.util.HashMap;
import java.util.List;
import java.util.Arrays;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.spzx.cart.api.RemoteCartService;
import com.spzx.cart.domain.CartInfo;
import com.spzx.common.core.domain.R;
import com.spzx.common.core.exception.ServiceException;
import com.spzx.common.core.utils.DateUtils;
import com.spzx.common.core.utils.StringUtils;
import com.spzx.common.core.utils.bean.BeanUtils;
import com.spzx.common.core.utils.uuid.UUID;
import com.spzx.common.rabbitmq.service.RabbitmqService;
import com.spzx.common.security.utils.SecurityUtils;
import com.spzx.order.domain.OrderItem;
import com.spzx.order.domain.OrderLog;
import com.spzx.order.domain.dto.OrderSubmitDTO;
import com.spzx.order.domain.vo.OrderTradeVO;
import com.spzx.order.service.IOrderItemService;
import com.spzx.order.service.IOrderLogService;
import com.spzx.product.api.RemoteProductService;
import com.spzx.product.domain.dto.SkuPriceDTO;
import com.spzx.product.domain.dto.SkuStockDTO;
import com.spzx.system.api.RemoteUserService;
import com.spzx.user.api.RemoteUserInfoService;
import com.spzx.user.domain.UserAddress;
import jakarta.annotation.Resource;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import com.spzx.order.mapper.OrderInfoMapper;
import com.spzx.order.domain.OrderInfo;
import com.spzx.order.service.IOrderInfoService;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;

/**
 * 订单Service业务层处理
 *
 * @author atguigu
 * @date 2024-10-25
 */
@Service
public class OrderInfoServiceImpl extends ServiceImpl<OrderInfoMapper, OrderInfo> implements IOrderInfoService
{
    @Resource
    private OrderInfoMapper orderInfoMapper;
    @Resource
    RemoteCartService remoteCartService;
    @Resource
    RemoteProductService remoteProductService;
    @Resource
    RemoteUserInfoService remoteUserInfoService;
    @Resource
    IOrderItemService orderItemService;
    @Resource
    IOrderLogService orderLogService;
    @Resource
    RedisTemplate redisTemplate;
    @Resource
    RabbitmqService rabbitmqService;

    /**
     * 查询订单列表
     *
     * @param orderInfo 订单
     * @return 订单
     */
    @Override
    public List<OrderInfo> selectOrderInfoList(OrderInfo orderInfo)
    {
        return orderInfoMapper.selectOrderInfoList(orderInfo);
    }
    //结算页
    @Override
    public OrderTradeVO trade() {
        //1、根据userId到购物车服务中 查询选中的购物项列表转为 订单项列表
        Long userId = SecurityUtils.getUserId();

        R<List<CartInfo>> cartListR = remoteCartService.cartListByUserId(userId);
        if(cartListR.getCode() != 200){
            throw new ServiceException(cartListR.getMsg());
        }
        List<CartInfo> cartInfoList = cartListR.getData();
        //将购物项转为订单项：  一个购物项对应一个订单项，一个订单包含多个订单项

        List<OrderItem> orderItemList = cartInfoList.stream().map(cartInfo -> {
            OrderItem orderItem = new OrderItem();
            //将数据源的相同名称类型的属性值拷贝给 目标对象
            BeanUtils.copyProperties(cartInfo , orderItem);
            orderItem.setSkuNum(cartInfo.getSkuNum().longValue());
            return orderItem;
        }).collect(Collectors.toList());
        //2、遍历购物项列表计算总金额
        BigDecimal totalAmount = orderItemList.stream()
                //计算每个订单项的小计金额返回
                .map(orderItem ->
                        orderItem.getSkuPrice().multiply(new BigDecimal(orderItem.getSkuNum())))
                //累加前面返回的所有小计金额
                .reduce((a, b) -> a.add(b))
                .get();//获取计算后的结果

        OrderTradeVO tradeVO = new OrderTradeVO();
        tradeVO.setTotalAmount(totalAmount);
        tradeVO.setOrderItemList(orderItemList);

        return tradeVO;
    }
    @Transactional(rollbackFor = Exception.class) //本地事务在分布式项目中会失效：分布式事务可以解决该问题！
    @Override
    public Long submitOrder(OrderSubmitDTO orderSubmitDTO) {
        //1、幂等性处理： 为了保证同一个页面多次提交请求后端只会处理一次(减少生成冗余数据)
        //结算页数据中包含一个唯一的tradeNo，可以通过他判断 请求是不是来自同一个页面
        String tradeNo = orderSubmitDTO.getTradeNo();
        if(StringUtils.isEmpty(tradeNo)){
            throw new ServiceException("非法请求");// 项目中配置了全局异常处理器！
        }
        //redis分布式锁：使用redis的setnx tradeNo当作key设置到redis中，成功表示第一次使用可以继续执行 失败表示tradeNo已经使用了 拒绝请求
        Boolean flag = redisTemplate.opsForValue().setIfAbsent("spzx:order:lock:" + tradeNo, "1",
                10, TimeUnit.MINUTES);
        if(!flag){
            //tradeNo的请求已经被处理过
            throw new ServiceException("订单已提交，请等待");
        }
        //2、验价： 结算页提交的商品价格是从购物车中查询到的，购物车中的数据是redis缓存，可能存在数据一致性问题
        // == 防止价格变化
        //一个订单有多个订单项，应该校验每一个订单项商品金额是否正确
        //可以 将所有要检验价格的数据封装到集合中 一次性提交给product服务校验价格  并返回校验后的结果
        /*
            验价DTO类：
                skuId: 可以查询到sku数据(包含最新价格)
                skuPrice: 订单项sku价格
                flag: 价格是否校验成功(true成功)
                skuName: 验价的商品标题(为了在验价失败时给用户提示)
         */
        //2.1 获取订单项集合
        List<OrderItem> orderItemList = orderSubmitDTO.getOrderItemList();
        if(CollectionUtils.isEmpty(orderItemList)){
            throw new ServiceException("订单数据错误");
        }
        //2.2 将订单集合转为验价对象集合
        List<SkuPriceDTO> skuPriceDTOS = orderItemList.stream().map(orderItem -> {
            SkuPriceDTO skuPriceDTO = new SkuPriceDTO();
//            skuPriceDTO.setSkuId(orderItem.getSkuId());
//            skuPriceDTO.setSkuPrice(orderItem.getSkuPrice());
//            skuPriceDTO.setSkuName(orderItem.getSkuName());
            BeanUtils.copyProperties(orderItem , skuPriceDTO);
            return skuPriceDTO;
        }).collect(Collectors.toList());
        //2.3 远程调用product服务验价 得到验价的结果
        R<List<SkuPriceDTO>> checkSkuPriceR = remoteProductService.checkSkuPrice(skuPriceDTOS);
        if(checkSkuPriceR.getCode()!=200){
            throw new ServiceException(checkSkuPriceR.getMsg());
        }
        skuPriceDTOS = checkSkuPriceR.getData();//获取验价的结果
        //获取验价失败的商品信息
        List<SkuPriceDTO> checkedFailedSkuPrices = skuPriceDTOS.stream()
                .filter(skuPriceDTO -> !skuPriceDTO.isFlag()).collect(Collectors.toList());
        if(!CollectionUtils.isEmpty(checkedFailedSkuPrices)){
            //存在价格校验失败的商品
            String skuNames = checkedFailedSkuPrices.stream()
                    .map(SkuPriceDTO::getSkuName)
                    .collect(Collectors.joining(","));
            throw new ServiceException("商品 "+skuNames+ " 价格变动，请刷新页面");
        }

        //代码执行到当前行表示验价通过:
        //3、验库存锁库存： 保证库存足够 然后锁定自己要购买的库存！
        /*
            库存数据保存： product服务 sku_stock表中存储
                total_num ： 商品总数量
                lock_num ：     锁定的库存数量
                available_num ： 可用库存
                sale_num：   销量

                - 锁库存的sql：
                    update sku_stock set lock_num = lock_num +skuNum , available_num = available_num - skuNum
                    where sku_id = skuId
                - 释放库存：用户取消了订单
                    update sku_stock set lock_num = lock_num -skuNum ,
                                    available_num = available_num + skuNum
                    where sku_id = skuId
                - 更新库存销量：用户支付成功
                    update sku_stock set lock_num = lock_num -skuNum ,
                        sale_num = sale_num + skuNum
                    where sku_id = skuId

                - 用户验库存锁库存：
                    判断available_num是否大于等于自己要购买的数量
                    可用库存available_num足够，可以修改available_num减去购买的数量，修改lock_num加上购买的数量
                        表示库存锁定
         */
        // 将订单项转为验库存的数据集合 远程访问product服务批量库存校验
        // SkuStockDTO: skuId , skuNum , skuName , flag（true表示验库存锁库存成功）, orderNo(以后用户取消订单释放订单锁定的库存的订单标号)
        //！=======预先生成订单编号
        String  orderNo = UUID.randomUUID().toString().replace("-","");
        //3.1 将订单详情转为验库存锁库存的对象集合
        List<SkuStockDTO> skuStockDTOS = orderItemList.stream()
                .map(orderItem -> {
                    SkuStockDTO skuStockDTO = new SkuStockDTO();
                    BeanUtils.copyProperties(orderItem, skuStockDTO);

                    //因为skuNum 两个类的该属性 数据类型不同 所以拷贝失败 需要在拷贝之后再手动设置该值
                    skuStockDTO.setSkuNum(orderItem.getSkuNum().intValue());
                    //！=======验库存对象 绑定订单编号
                    skuStockDTO.setOrderNo(orderNo);
                    return skuStockDTO;
                }).collect(Collectors.toList());
        //3.2 远程调用product服务验库存锁库存接口校验锁定库存
        R<List<SkuStockDTO>> checkAndLockSkuStockR = remoteProductService.checkAndLockSkuStock(skuStockDTOS);
        if(checkAndLockSkuStockR.getCode()!=200){
            throw new ServiceException(checkAndLockSkuStockR.getMsg());
        }
        //获取验完库存锁定库存的结果
        skuStockDTOS = checkAndLockSkuStockR.getData();
        List<SkuStockDTO> lockedStockFailedDTOS = skuStockDTOS.stream() //获取锁定失败的集合
                .filter(skuStockDTO -> !skuStockDTO.isFlag()).collect(Collectors.toList());
        if(!CollectionUtils.isEmpty(lockedStockFailedDTOS)){
            //存在锁定库存失败的订单项
            String skuNames = lockedStockFailedDTOS.stream().map(SkuStockDTO::getSkuName)
                    .collect(Collectors.joining(","));
            throw new ServiceException("商品 "+skuNames+" 库存不足");
        }

        //4、保存订单数据order_info表

        OrderInfo orderInfo = new OrderInfo();
        orderInfo.setUserId(SecurityUtils.getUserId());
        orderInfo.setNickName(SecurityUtils.getUsername());
        orderInfo.setOrderNo(orderNo);//订单号和订单id都代表唯一的订单，订单号侧重 跟三方交互时使用

        orderInfo.setCouponId(null);//没有优惠券
        orderInfo.setCouponAmount(new BigDecimal(0));
        //计算订单总金额
        BigDecimal totalAmount = orderItemList.stream()
                .map(orderItem -> orderItem.getSkuPrice()
                        .multiply(new BigDecimal(orderItem.getSkuNum())))
                .reduce((a, b) -> a.add(b)).get();
        orderInfo.setTotalAmount(totalAmount); //订单总金额： 订单项累加金额-优惠券金额
        orderInfo.setOriginalTotalAmount(totalAmount); //订单项累加金额
        orderInfo.setFeightFee(new BigDecimal(0));
        orderInfo.setOrderStatus(0L);//订单状态  默认待付款

        //远程访问user服务：根据收货地址查询收货地址详情
        R<UserAddress> userAddressR = remoteUserInfoService.getById(orderSubmitDTO.getUserAddressId());
        if(userAddressR.getCode()!=200){
            throw new ServiceException(userAddressR.getMsg());
        }
        UserAddress userAddress = userAddressR.getData();
        orderInfo.setReceiverAddress(userAddress.getFullAddress());
        orderInfo.setReceiverName(userAddress.getName());
        orderInfo.setReceiverPhone(userAddress.getPhone());
        orderInfo.setReceiverTagName(userAddress.getTagName());
        orderInfo.setReceiverProvince(userAddress.getProvinceCode());
        orderInfo.setReceiverCity(userAddress.getCityCode());
        orderInfo.setReceiverDistrict(userAddress.getDistrictCode());


        orderInfo.setRemark(orderInfo.getRemark());
        baseMapper.insert(orderInfo);
        //5、保存订单项数据到数据库order_item表
        Long orderId = orderInfo.getId();//获取保存成功的订单id
        orderItemList.forEach(orderItem -> {
            orderItem.setOrderId(orderId);
        });
        orderItemService.saveBatch(orderItemList);

        //6、删除购物车中选中的购物项！！ 使用mq异步解耦合 不再直接远程调用
        /*
            问题：
                同步阻塞执行:
                    非核心业务(和主业务不具有原子性)，没有必要同步阻塞执行
                        - 可以异步任务、异步编排执行
                代码耦合：
                    需要再order服务的业务方法中调用 购物车服务的feign客户端方法实现功能
             Rabbitmq:
                为什么使用他？
                    异步解耦合
                如何使用？
                    生产者发送消息到mq
                    消费者消费消息执行对应的业务
                五大消息模型：
                    simple： 一个生产者 一个消费者 一个队列
                            消息有序
                               mq如何保证消息的有序性： simple+ 消费者单线程消费
                    work： 工作   一个生产者 多个消费者  一个队列
                            队列中的每个消息只能被一个消费者消费一次
                            多个消费者会争抢消息消费
                            > 能者多劳： 配置prefetch执行消费者的消费能力，一次分发几个消息
                    fanout：广播  生产者    广播交换机  多个队列 对应每个队列的多个消费者
                            - 生产者发送消息到广播交换机
                            - 消费者通过队列绑定到交换机上 才可以接收到交换机分发的消息，消费者可以消费该消息
                            特点： 队列只要绑定了交换机 就可以接收到消息
                                    每个队列的一个消息只能被消费一次
                            - 交换机 不存数据 如果没有绑定的队列 消息会被丢弃
                    direct：定向   direct交换机  多个队列 对应每个队列的多个消费者
                        - 生产者发送消息时绑定一个routingkey，消息发送到定向交换机
                        - 绑定到交换机上的队列，如果绑定时的routingkey和消息的routingkey一样，消息才会发送到队列中
                    topic： 主题 使用最多！
                        - routingkey支持通配符   *表示一级路径任意多个字符，#表示任意多级路径任意多个字符
         */
//        remoteCartService.deleteCheckedCartInfo(SecurityUtils.getUserId());
        //7、保存订单日志
        orderLogService.saveLogAsync(orderInfo);

        //8、发送订单提交成功的消息
        // -业务1：删除购物车中的购物项：需要使用创建订单的用户id
        // -业务2：给用户发送短信通知： 需要使用用户id、订单id,订单编号、订单金额..
        Map<String,Object> map = new HashMap<>();
        map.put("userId" , SecurityUtils.getUserId());
        map.put("orderId" , orderId);
        map.put("orderNo" , orderNo);
        map.put("totalAmount" , totalAmount);
        rabbitmqService.sendMsg("spzx.order.exchange",
                "spzx.order.submit.success",
                map);//使用上面的map当作消息内容
        //9、延迟关单
        // 用户下单时锁定了库存，如果超过允许的时间未支付 可以关闭订单(将订单状态修改为关闭状态)、还需要释放锁定的库存
        //返回订单id！
        return orderId;
    }

    @Override
    public int  cancelOrder(Long orderId) {
        //取消订单: 不需要延迟执行  直接发送到延迟队列中
        //直接发送关单消息到延迟队列中，消费者会立即消费消息
        OrderInfo orderInfo = baseMapper.selectById(orderId);
        if(orderInfo==null || orderInfo.getOrderStatus().intValue()!=0){
            throw new ServiceException("订单取消失败");
        }
        Map<String,Object> map = new HashMap<>();
        map.put("userId" , SecurityUtils.getUserId());
        map.put("orderId" , orderId);
        map.put("orderNo" , orderInfo.getOrderNo());
        map.put("totalAmount" , orderInfo.getTotalAmount());
//                        .deadLetterExchange("spzx.order.delay.exchange") //死信交换机
//                .deadLetterRoutingKey("spzx.order.delay.msg") //死信路由key
        rabbitmqService.sendMsg("spzx.order.delay.exchange",
                "spzx.order.delay.msg",
                map);

        return 1;
    }

}
